﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace TreeBuilder
{
    public class BitmapBuilder
    {
        private static readonly Dictionary<string, Type> ExtensionMapping =
            new Dictionary<string, Type>
                {
                    {"bmp", typeof (BmpBitmapEncoder)},
                    {"jpeg", typeof (JpegBitmapEncoder)},
                    {"png", typeof (PngBitmapEncoder)}
                };


        private Thickness Margin { get; set; }

        private double _dpiX;
        private double _dpiY;

        public BitmapBuilder(Thickness margin, double dpiX = 220, double dpiY = 220)
        {
            Margin = margin;
            _dpiX = dpiX;
            _dpiY = dpiY;
        }

        public void SaveImage(Tree tree, string path, string extension)
        {
            BitmapSource bitmap = BuildImage(tree);

            Type encoderType = ExtensionMapping[extension];
            var encoder = (BitmapEncoder)encoderType.GetConstructor(new Type[0]).Invoke(new object[0]);

            SaveToFile(bitmap, encoder, path);
        }


        private static void SaveToFile(BitmapSource bitmap, BitmapEncoder encoder, string path)
        {
            using (FileStream stream = File.Create(path))
            {
                encoder.Frames.Add(BitmapFrame.Create(bitmap));
                encoder.Save(stream);
                stream.Flush();
            }
        }

        public BitmapSource BuildImage(Tree tree)
        {
            Rect treeRect = GetTreeBounds(tree);

            var canvas = tree.TreeCanvas;

            PrepareTree(tree);

            //+2, иначе границы не видны
            var size = new Size((treeRect.X + treeRect.Width + 2),
                                (treeRect.Y + treeRect.Height + 2));
            RedrawCanvas(canvas, size);
            RenderTargetBitmap image;
            try
            {
                image = new RenderTargetBitmap(
                    (int) (size.Width),
                    (int) (size.Height),
                    _dpiX,
                    _dpiY,
                    PixelFormats.Default);

                image.Render(canvas);
            }
            finally
            {
                UnprepareTree(tree);

                Size previous = new Size(tree.ActualWidth, tree.ActualHeight);
                RedrawCanvas(canvas, previous);   
            }

            CroppedBitmap cropped = CropImage(image, treeRect);

            BitmapSource framed = AddFrame(cropped);

            return framed;
        }

        private BitmapSource AddFrame(BitmapSource image)
        {
            var writeable = new WriteableBitmap(image.PixelWidth + (int) Margin.Left + (int) Margin.Right,
                                             image.PixelHeight + (int) Margin.Top + (int) Margin.Bottom, 
                                             image.DpiX, image.DpiY,
                                             image.Format, image.Palette);

            byte[] initialPixels = new byte[writeable.PixelWidth*writeable.PixelHeight*writeable.Format.BitsPerPixel/8];
            for (int i = 0; i < initialPixels.Length; i++)
                initialPixels[i] = byte.MaxValue;

            writeable.WritePixels(new Int32Rect(0,0, writeable.PixelWidth, writeable.PixelHeight), initialPixels, writeable.PixelWidth * writeable.Format.BitsPerPixel/8, 0);

            byte[] pixels = new byte[image.PixelWidth*image.PixelHeight*image.Format.BitsPerPixel/8];
            image.CopyPixels(pixels, image.PixelWidth * image.Format.BitsPerPixel / 8, 0);

            writeable.WritePixels(new Int32Rect((int) Margin.Left, (int) Margin.Top, image.PixelWidth, image.PixelHeight),
                               pixels,
                               image.PixelWidth*image.Format.BitsPerPixel/8,
                               0);

            return writeable;
        }

        private CroppedBitmap CropImage(BitmapSource image, Rect treeRect)
        {
            var rect = new Int32Rect((int) Math.Floor(treeRect.X), (int) Math.Floor(treeRect.Y),
                                     (int) Math.Ceiling(treeRect.Width), (int) Math.Ceiling(treeRect.Height));
            var cropped = new CroppedBitmap(image, rect);
            return cropped;
        }

        private void UnprepareTree(Tree tree)
        {
            tree.TreeCanvas.DottedEnabled = true;

            foreach (var element in tree.TreeCanvas.Children.OfType<IPrintable>())
            {
                element.IsBeingPrinted = false;
                var size = (element as UIElement).RenderSize;
                (element as UIElement).Measure(size);
                (element as UIElement).Arrange(new Rect(size));
            }
        }

        private void PrepareTree(Tree tree)
        {
            tree.TreeCanvas.Focus();
            tree.TreeCanvas.DottedEnabled = false;

            foreach (var element in tree.TreeCanvas.Children.OfType<IPrintable>())
            {
                element.IsBeingPrinted = true;
                var size = (element as UIElement).RenderSize;
                (element as UIElement).Measure(size);
                (element as UIElement).Arrange(new Rect(size));
            }
        }

        private static void RedrawCanvas(Canvas canvas, Size size)
        {
            canvas.Measure(size);
            canvas.Arrange(new Rect(size));
        }

        public Rect GetTreeBounds(Tree tree)
        {
            double minX, minY;
            double maxX, maxY;
            GetNodeCoordinates(tree, out minX, out minY, out maxX, out maxY);

            GetBinderBounds(tree, ref maxY, ref minX, ref maxX);

            maxY = NormalizeCoordinates(maxY, ref minX, ref maxX, ref minY);

            double width = maxX - minX;
            double height = maxY - minY;

            if (width < 0 || height < 0)
                throw new Exception("Invalid width or height.");

            return new Rect(minX, minY, width, height);
        }

        private void GetBinderBounds(Tree tree, ref double maxY, ref double minX, ref double maxX)
        {
            IEnumerable<ArrowLink> arrowLinks = tree.TreeCanvas.Children.OfType<ArrowLink>();
            if (arrowLinks.Count() > 0)
            {
                double linksMaxY = arrowLinks.Max(x => GetBottomCoordinate(x));
                maxY = Math.Max(maxY, linksMaxY);

                double linksMinX = arrowLinks.Min(x => GetLeftCoordinate(x));
                minX = Math.Min(minX, linksMinX);

                double linksMaxX = arrowLinks.Max(x => GetRightCoordinate(x));
                maxX = Math.Max(maxX, linksMaxX);
            }

            IEnumerable<TriangleLink> triangles = tree.TreeCanvas.Children.OfType<TriangleLink>();
            if (triangles.Count() > 0)
            {
                double minElementX = triangles.Min(
                    x => Math.Min(x.To.LeftCenter.X, x.From.LeftCenter.X) - x.Line.StrokeThickness
                    );
                double maxElementX =
                    triangles.Max(x => Math.Max(x.To.RightCenter.X, x.From.RightCenter.X) + x.Line.StrokeThickness);
                maxX = Math.Max(maxX, maxElementX);
                minX = Math.Min(minX, minElementX);
            }
        }

        private double NormalizeCoordinates(double maxY, ref double minX, ref double maxX, ref double minY)
        {
            minX = Math.Max(0, minX);
            minY = Math.Max(0, minY);

            minX = minX * _dpiX / 96;
            minY = minY * _dpiY / 96;

            maxX = maxX * _dpiX / 96;
            maxY = maxY * _dpiX / 96;
            return maxY;
        }

        private void GetNodeCoordinates(Tree tree, out double minX, out double minY, out double maxX, out double maxY)
        {
            var nodes = 
                tree.TreeCanvas.Children
                    .OfType<object>()
                    .Where(x => x is INode)
                    .Select(x => (INode) x)
                    .ToArray();

            if (nodes.Length <= 0)
            {
                minX = minY = maxX = maxY = 0;
                return;
            }

            minX = nodes.Min(x => x.LeftCenter.X);
            minY = nodes.Min(x => x.Center.Y);

            maxY = nodes.Max(x => x.BottomCenter.Y);
            maxX = nodes.Max(x => x.RightCenter.X);

            var indexes = tree.TreeCanvas.Children.OfType<IndexBlock>().ToList();
            if (indexes.Count <= 0)
                return;

            double minIndexX = indexes.Min(x => Canvas.GetLeft(x));
            double minIndexY = indexes.Min(x => Canvas.GetTop(x));

            double maxIndexX = indexes.Max(x => Canvas.GetLeft(x) + x.ActualWidth);
            double maxIndexY = indexes.Max(x => Canvas.GetTop(x) + x.ActualHeight);

            minX = Math.Min(minX, minIndexX);
            minY = Math.Min(minY, minIndexY);

            maxX = Math.Max(maxX, maxIndexX);
            maxY = Math.Max(maxY, maxIndexY);
        }

        private double GetLeftCoordinate(ArrowLink arrow)
        {
            double headLeft = Canvas.GetLeft(arrow.Head);
            double selfLeft = Math.Min(arrow.From.BottomCenter.X, arrow.To.BottomCenter.X);
            return Math.Min(headLeft, selfLeft);
        }

        private double GetRightCoordinate(ArrowLink arrow)
        {
            double headRight = Canvas.GetLeft(arrow.Head) + arrow.Head.ActualWidth;
            double selfRight = Math.Max(arrow.From.BottomCenter.X, arrow.To.BottomCenter.X);
            return Math.Max(headRight, selfRight);
        }

        private static double GetBottomCoordinate(ArrowLink arrow)
        {
            return arrow.RenderSize.Height;
        }
    }
}
